/*
 * $Id: AbstractDateSelectionModel.java 3927 2011-02-22 16:34:11Z kleopatra $
 *
 * Copyright 2006 Sun Microsystems, Inc., 4150 Network Circle,
 * Santa Clara, California 95054, U.S.A. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */
package org.jdesktop.swingx.calendar;

import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.SortedSet;
import java.util.TimeZone;
import java.util.TreeSet;

import org.jdesktop.swingx.event.DateSelectionEvent;
import org.jdesktop.swingx.event.DateSelectionEvent.EventType;
import org.jdesktop.swingx.event.DateSelectionListener;
import org.jdesktop.swingx.event.EventListenerMap;

/**
 * Abstract base implementation of DateSelectionModel. Implements notification,
 * Calendar related properties and lower/upper bounds.
 * 
 * @author Jeanette Winzenburg
 */
public abstract class AbstractDateSelectionModel implements DateSelectionModel {
	public static final SortedSet<Date> EMPTY_DATES = Collections.unmodifiableSortedSet(new TreeSet<Date>());

	protected EventListenerMap listenerMap;
	protected boolean adjusting;
	protected Calendar calendar;
	protected Date upperBound;
	protected Date lowerBound;

	/**
	 * the locale used by the calendar.
	 * <p>
	 * NOTE: need to keep separately as a Calendar has no getter.
	 */
	protected Locale locale;

	/**
	 * Instantiates a DateSelectionModel with default locale.
	 */
	public AbstractDateSelectionModel() {
		this(null);
	}

	/**
	 * Instantiates a DateSelectionModel with the given locale. If the locale is
	 * null, the Locale's default is used.
	 * 
	 * PENDING JW: fall back to JComponent.getDefaultLocale instead? We use this
	 * with components anyway?
	 * 
	 * @param locale
	 *            the Locale to use with this model, defaults to
	 *            Locale.default() if null.
	 */
	public AbstractDateSelectionModel(Locale locale) {
		this.listenerMap = new EventListenerMap();
		setLocale(locale);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public Calendar getCalendar() {
		return (Calendar) calendar.clone();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public int getFirstDayOfWeek() {
		return calendar.getFirstDayOfWeek();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void setFirstDayOfWeek(final int firstDayOfWeek) {
		if (firstDayOfWeek == getFirstDayOfWeek())
			return;
		calendar.setFirstDayOfWeek(firstDayOfWeek);
		fireValueChanged(EventType.CALENDAR_CHANGED);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public int getMinimalDaysInFirstWeek() {
		return calendar.getMinimalDaysInFirstWeek();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void setMinimalDaysInFirstWeek(int minimalDays) {
		if (minimalDays == getMinimalDaysInFirstWeek())
			return;
		calendar.setMinimalDaysInFirstWeek(minimalDays);
		fireValueChanged(EventType.CALENDAR_CHANGED);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public TimeZone getTimeZone() {
		return calendar.getTimeZone();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void setTimeZone(TimeZone timeZone) {
		if (getTimeZone().equals(timeZone))
			return;
		TimeZone oldTimeZone = getTimeZone();
		calendar.setTimeZone(timeZone);
		adjustDatesToTimeZone(oldTimeZone);
		fireValueChanged(EventType.CALENDAR_CHANGED);
	}

	/**
	 * Adjusts all stored dates to a new time zone. This method is called after
	 * the change had been made.
	 * <p>
	 * 
	 * This implementation resets all dates to null, clears everything.
	 * Subclasses may override to really map to the new time zone.
	 *
	 * @param oldTimeZone
	 *            the old time zone
	 * 
	 */
	protected void adjustDatesToTimeZone(TimeZone oldTimeZone) {
		clearSelection();
		setLowerBound(null);
		setUpperBound(null);
		setUnselectableDates(EMPTY_DATES);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public Locale getLocale() {
		return locale;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void setLocale(Locale locale) {
		if (locale == null) {
			locale = Locale.getDefault();
		}
		if (locale.equals(getLocale()))
			return;
		this.locale = locale;
		if (calendar != null) {
			calendar = Calendar.getInstance(calendar.getTimeZone(), locale);
		} else {
			calendar = Calendar.getInstance(locale);
		}
		fireValueChanged(EventType.CALENDAR_CHANGED);
	}

	// ------------------- utility methods

	/**
	 * Returns the start of the day of the given date in this model's calendar.
	 * NOTE: the calendar is changed by this operation.
	 * 
	 * @param date
	 *            the Date to get the start for.
	 * @return the Date representing the start of the day of the input date.
	 */
	protected Date startOfDay(Date date) {
		return CalendarUtils.startOfDay(calendar, date);
	}

	/**
	 * Returns the end of the day of the given date in this model's calendar.
	 * NOTE: the calendar is changed by this operation.
	 * 
	 * @param date
	 *            the Date to get the start for.
	 * @return the Date representing the end of the day of the input date.
	 */
	protected Date endOfDay(Date date) {
		return CalendarUtils.endOfDay(calendar, date);
	}

	/**
	 * Returns a boolean indicating whether the given dates are on the same day
	 * in the coordinates of the model's calendar.
	 * 
	 * @param selected
	 *            one of the dates to check, must not be null.
	 * @param compare
	 *            the other of the dates to check, must not be null.
	 * @return true if both dates represent the same day in this model's
	 *         calendar.
	 */
	protected boolean isSameDay(Date selected, Date compare) {
		return startOfDay(selected).equals(startOfDay(compare));
	}

	// ------------------- bounds

	/**
	 * {@inheritDoc}
	 */
	@Override
	public Date getUpperBound() {
		return upperBound;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void setUpperBound(Date upperBound) {
		if (upperBound != null) {
			upperBound = getNormalizedDate(upperBound);
		}
		if (CalendarUtils.areEqual(upperBound, getUpperBound()))
			return;
		this.upperBound = upperBound;
		if (this.upperBound != null && !isSelectionEmpty()) {
			long justAboveUpperBoundMs = this.upperBound.getTime() + 1;
			removeSelectionInterval(new Date(justAboveUpperBoundMs), getLastSelectionDate());
		}
		fireValueChanged(EventType.UPPER_BOUND_CHANGED);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public Date getLowerBound() {
		return lowerBound;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void setLowerBound(Date lowerBound) {
		if (lowerBound != null) {
			lowerBound = getNormalizedDate(lowerBound);
		}
		if (CalendarUtils.areEqual(lowerBound, getLowerBound()))
			return;
		this.lowerBound = lowerBound;
		if (this.lowerBound != null && !isSelectionEmpty()) {
			// Remove anything below the lower bound
			long justBelowLowerBoundMs = this.lowerBound.getTime() - 1;
			removeSelectionInterval(getFirstSelectionDate(), new Date(justBelowLowerBoundMs));
		}
		fireValueChanged(EventType.LOWER_BOUND_CHANGED);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public boolean isAdjusting() {
		return adjusting;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void setAdjusting(boolean adjusting) {
		if (adjusting == isAdjusting())
			return;
		this.adjusting = adjusting;
		fireValueChanged(adjusting ? EventType.ADJUSTING_STARTED : EventType.ADJUSTING_STOPPED);

	}

	// ----------------- notification
	/**
	 * {@inheritDoc}
	 */
	@Override
	public void addDateSelectionListener(DateSelectionListener l) {
		listenerMap.add(DateSelectionListener.class, l);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void removeDateSelectionListener(DateSelectionListener l) {
		listenerMap.remove(DateSelectionListener.class, l);
	}

	public List<DateSelectionListener> getDateSelectionListeners() {
		return listenerMap.getListeners(DateSelectionListener.class);
	}

	protected void fireValueChanged(DateSelectionEvent.EventType eventType) {
		List<DateSelectionListener> listeners = getDateSelectionListeners();
		DateSelectionEvent e = null;

		for (DateSelectionListener listener : listeners) {
			if (e == null) {
				e = new DateSelectionEvent(this, eventType, isAdjusting());
			}
			listener.valueChanged(e);
		}
	}

}
